package org.oy.sealogger.device.gps;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.concurrent.Semaphore;

import org.apache.log4j.Logger;
import org.oy.sealogger.common.Config;
import org.oy.sealogger.common.Messages;
import org.oy.sealogger.device.Device;
import org.oy.sealogger.device.DeviceException;
import org.oy.sealogger.ds.InputMetaData;
import org.oy.sealogger.ds.InputAnalyser;

/**
 * Implementation of GPSDevice interface for NMEA0183 input in Linux,
 * where we just plug in a device and read /dev/ttyUSB0 file.
 * @author riku
 * 
 */
public class GPSLinuxFileDevice implements Device {
	
	private static final String serialPrefix = "/dev/ttyS";
	private static final String usbPrefix = "/dev/ttyUSB";

	private static final int serialMax = 2;
	private static final int usbMax = 20;
	
	private volatile static String[] portNames;
	private volatile int i = 0;
	private volatile Vector<File> handles;
	private Semaphore threadsFinished;
	
	private InputStream in;
	private Logger logger;
	private InputAnalyser ia;
	
	private boolean connected = false;
	
	public GPSLinuxFileDevice() {
		logger = Config.getLogger();
	}
	
	public void connect() throws GPSDeviceException {
		
		scanPorts();
		File device = null;
		
		if(handles.size() == 0) {
			device = null;
			connected = false;
		} else if(handles.size() == 1) {
			device = handles.get(0);
			try {
				in = new FileInputStream(device);
				InputMetaData md = ia.analyse(in);
				
				if(!md.isOfWantedType()) {
					in.close();
					device = null;
					connected = false;
				} else {
					connected = true;
				}
				
			} catch(FileNotFoundException e) {
				logger.error(Messages.getMessage("error.file_not_found"), e);
			} catch(IOException e) {
				logger.error(Messages.getMessage("error.cannot_close"), e);
			}
		} else if(handles.size() > 1) {
			for(int i = 0; i < handles.size(); ++i) {
				try {
					in = new FileInputStream(handles.get(i));
					InputMetaData md = ia.analyse(in);
					
					if(md.isOfWantedType()) {
						device = handles.get(i);
						connected = true;
						break;
					} else {
						connected = false;
						in.close();
					}
					
				} catch (FileNotFoundException e) {
					logger.error(Messages.getMessage("error.file_not_found"));
				} catch(IOException e) {
					logger.error(Messages.getMessage("error.cannot_close"));
				}
			}
		}
		
		if(device == null) throw new GPSDeviceException(Messages.getMessage("error.cannot_connect"));		
	}

	public void disconnect() throws GPSDeviceException {
		try {
			in.close();
		} catch(IOException ioe) {
			logger.error(Messages.getMessage("error.cannot_close"), ioe);
		} 
		
		connected = false;
	}

	public InputStream getInputStream() {
		return in;
	}
	
	private void scanPorts() {
		
		portNames = createPortNameArray();
		handles = new Vector<File>(portNames.length);
		threadsFinished = new Semaphore(portNames.length);
		
		try {
			// Acquire all available semaphore permits
			//logger.debug("Acquiring the semaphore permits ...");
			threadsFinished.acquire(portNames.length);
		} catch(InterruptedException ie) {
			logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), ie);
		}
		
		for(i = 0; i < portNames.length; ++i) {
			Thread scanThread = new ScanThread(i);
			scanThread.start();
		}
		
		try {
			// Acquire all the semaphore permits again
			//logger.debug("Acquiring the semaphore permits ...");
			threadsFinished.acquire(portNames.length);
			//logger.debug("Semaphores acquired");
		} catch (InterruptedException e) {
			logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), e);
		} finally {
			//logger.debug("Releasing the semaphore permits ...");
			threadsFinished.release(portNames.length);
			//logger.debug(handles.size() + " active ports");
		}
	}

	private String[] createPortNameArray() {
		
		String[] ports = new String[usbMax + serialMax];
		int index = 0;
		
		for(int i = 0; i < usbMax; ++i) {
			ports[index++] = usbPrefix + i;
			//logger.debug(usbPrefix + i);
		}
		
		for(int i = 0; i < serialMax; ++i) {
			ports[index++] = serialPrefix + i;
			//logger.debug(serialPrefix + i);
		}
		
		return ports;
	}
	
	private class ScanThread extends Thread {
		
		private int portNameIndex;
		
		public ScanThread(int portNameIndex) {
			this.portNameIndex = portNameIndex;
		}
		
		public void run() {
			try {
				File handle = new File(portNames[portNameIndex]);
				
				if(handle.exists() && handle.canRead()) {
					FileInputStream in = new FileInputStream(handle);
					Thread.sleep(Config.DEVICE_DATA_WAIT_TIME);
					if(in.available() > 0) {
						logger.debug("Found a candidate port: " + portNames[portNameIndex]);
						handles.add(handle);
					}
				}
			} catch (InterruptedException e) {
				logger.error(Messages.getMessage("error.unexpected_thread_interrupt"), e);
			} catch(FileNotFoundException e) {
				logger.error(Messages.getMessage("error.file_not_found" + portNames[i]), e);
			} catch(IOException e) {
				logger.error(Messages.getMessage("error.cannot_read"), e);
			}
			
			//logger.debug("Releasing a semaphore permit ...");
			threadsFinished.release();	
		}
	}

	public boolean isConnected() {
		return connected;
	}

	public boolean dataAvailable() {
		return true;
	}

	@SuppressWarnings("finally")
	public boolean detect() {
		
		boolean deviceDetected = false;
		
		try {
			connect();
			deviceDetected = true;
		} catch(GPSDeviceException e) {
			logger.error(Messages.getMessage("error.no_device_detected"), e);
			disconnect();
		} finally {
			return deviceDetected;
		}
		
	}

	public void setInputAnalyser(InputAnalyser ia) {
		this.ia = ia;
	}

	@SuppressWarnings("finally")
	public boolean test() {
		
		boolean good = false;
		
		try {
			connect();
			good = true;			
			disconnect();
			
		} catch(DeviceException e) {
			logger.error(e);
		} finally {
			return good;
		}
	}

	public String getDeviceName() {
		return "File Device";
	}
}
